using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using System.Text;
using System.Web.UI.WebControls;

namespace Developex.DatabaseWrapper
{
	/// <summary>
	/// List of database objects
	/// </summary>
	/// <typeparam name="T"></typeparam>
	public abstract class BaseRowList<T> where T : Row
	{
		#region Public members/properties

		/// <summary>
		/// Loaded items
		/// </summary>
		public List<T> Items = new List<T>();

		/// <summary>
		/// Table name (get from sample item)
		/// </summary>
		public string TableName
		{
			get { return SampleItem.TableName; }
		}

		private DatabaseException lastException = null;

		/// <summary>
		/// Get last exception
		/// </summary>
		/// <returns></returns>
		public DatabaseException LastException
		{
			get { return lastException; }
		}

		private bool autoOrderById = true;

		/// <summary>
		/// Automatically add ORDER BY primary key
		/// </summary>
		public bool AutoOrderById
		{
			get { return autoOrderById; }
			set { autoOrderById = value; }
		}

		private bool isLoadChildValues;

		///<summary>
		/// Load child values for each loaded item
		///</summary>
		public bool IsLoadChildValues
		{
			get { return isLoadChildValues; }
			set { isLoadChildValues = value; }
		}

		#endregion

		#region Public functions

		/// <summary>
		/// Convert Items to dictionary, Key is PrimaryKeyField
		/// </summary>
		/// <typeparam name="TKey">type of Key</typeparam>
		/// <returns></returns>
		public Dictionary<TKey, T> FillDictionary<TKey>()
		{
			Dictionary<TKey, T> items = new Dictionary<TKey, T>(Items.Count);
			foreach (T item in Items)
			{
				items.Add((TKey)item.PrimaryKeyField.GetValue(), item);
			}
			return items;
		}

		///<summary>
		/// Convert Items to dictionary
		/// </summary>
		///<param name="keyPropertyName">Field name that will be used as Key</param>
		/// <typeparam name="TKey">type of Key</typeparam>
		///<returns></returns>
		public Dictionary<TKey, T> FillDictionary<TKey>(Enum keyPropertyName)
		{
			string keyFieldName = SampleItem.GetFieldNameByPropertyName(keyPropertyName.ToString());
			Dictionary<TKey, T> items = new Dictionary<TKey, T>(Items.Count);
			foreach (T item in Items)
			{
				items.Add((TKey)item.Fields[keyFieldName].GetValue(), item);
			}
			return items;
		}

		/// <summary>
		/// Check if item is already presented in collection (by primary key)
		/// </summary>
		/// <param name="itemToCompare"></param>
		/// <returns></returns>
		public bool ContainsByPrimaryKey(T itemToCompare)
		{
			foreach (T item in Items)
			{
				if (item.CompareByPrimaryKey(itemToCompare))
					return true;
			}
			return false;
		}

		/// <summary>
		/// Check if item is already presented in collection (by assigned fields)
		/// </summary>
		/// <param name="itemToCompare"></param>
		/// <returns></returns>
		public bool ContainsByAssignedFields(T itemToCompare)
		{
			foreach (T item in Items)
			{
				if (itemToCompare.CompareByAssignedFields(item))
					return true;
			}
			return false;
		}
		/// <summary>
		/// Load by list of primary keys
		/// </summary>
		/// <param name="pkList"></param>
		/// <param name="conn"></param>
		/// <returns></returns>
		public bool LoadBySinglePrimaryKeyList(Connection conn, IEnumerable pkList)
		{
			ClearInnerState();
			foreach (object pk in pkList)
			{
				T item = CreateObject();
				if (item.LoadByPrimaryKey(conn, pk) != OperationResult.Success)
					return false;
				Items.Add(item);
			}
			return true;
		}

		/// <summary>
		/// Load items by assigned fields
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="obj"></param>
		/// <returns></returns>
		public bool LoadByAssignedFields(Connection conn, T obj)
		{
			return LoadFromQueryBuilder(conn, obj.GetLoadByAssignedFieldsQB(conn));
		}

		/// <summary>
		/// Load items by assigned fields
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="obj"></param>
		/// <param name="pageIndex"></param>
		/// <param name="pageSize"></param>
		/// <param name="itemsCount"></param>
		/// <returns></returns>
		public bool LoadByAssignedFields(Connection conn, T obj, int pageIndex, int pageSize, out int itemsCount)
		{
			return LoadFromQueryBuilder(conn, obj.GetLoadByAssignedFieldsQB(conn), pageIndex, pageSize, out itemsCount);
		}

		#endregion

		#region Base Public functions related to database queries

		/// <summary>
		/// Load items by query builder
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="qb"></param>
		/// <returns></returns>
		public List<T> LoadItemsFromQueryBuilder(Connection conn, QueryBuilder qb)
		{
			return LoadFromQueryBuilder(conn, qb) ? Items : null;
		}

		/// <summary>
		/// Load by query builder
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="qb"></param>
		/// <returns></returns>
		public bool LoadFromQueryBuilder(Connection conn, QueryBuilder qb)
		{
			return LoadFromSql(conn, qb.ToString(), qb.Parameters.ToArray());
		}

		/// <summary>
		/// Load items by sql
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="query"></param>
		/// <param name="parameters"></param>
		/// <returns></returns>
		public List<T> LoadItemsFromSql(Connection conn, string query, params IDataParameter[] parameters)
		{
			return LoadFromSql(conn, query, CommandType.Text, parameters) ? Items : null;
		}

		/// <summary>
		/// Load by sql
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="query"></param>
		/// <param name="parameters"></param>
		/// <returns></returns>
		public bool LoadFromSql(Connection conn, string query, params IDataParameter[] parameters)
		{
			return LoadFromSql(conn, query, CommandType.Text, parameters);
		}

		/// <summary>
		/// Load items by sql
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="query"></param>
		/// <param name="commandType"></param>
		/// <param name="parameters"></param>
		/// <returns></returns>
		public List<T> LoadItemsFromSql(Connection conn, string query, CommandType commandType, params IDataParameter[] parameters)
		{
			return LoadFromSql(conn, query, commandType, parameters) ? Items : null;
		}

		/// <summary>
		/// Load by sql
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="query"></param>
		/// <param name="commandType"></param>
		/// <param name="parameters"></param>
		/// <returns></returns>
		public bool LoadFromSql(Connection conn, string query, CommandType commandType, params IDataParameter[] parameters)
		{
			ClearInnerState();
			try
			{
				using (IDataReader reader = conn.ExecuteDataReader(query, commandType, parameters))
				{
					while (reader.Read())
					{
						T item = CreateObject();
						item.Load(conn, reader);
						Items.Add(item);
					}
				}
				if (IsLoadChildValues)
				{
					foreach (T item in Items)
					{
						foreach (IChildValuesField childValuesField in item.ChildValuesFields)
						{
							if (childValuesField.OnLoad(conn) != OperationResult.Success)
								return false;
						}
					}
				}
			}
			catch (DatabaseException)
			{
				// exception should be passed in event handler earlier
				// OnErrorOccured(conn, ex);

				return false;
			}
			catch (Exception ex)
			{
				OnErrorOccured(conn, new DatabaseException("", ex, "BaseRowList.LoadFromSql(Connection, Query, Parameters)", GetType()));
				return false;
			}
			return true;
		}

		/// <summary>
		/// Load page
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="qb"></param>
		/// <param name="pageIndex">starts with 1</param>
		/// <param name="pageSize"></param>
		/// <param name="itemsCount"></param>
		/// <returns></returns>
		public List<T> LoadItemsFromQueryBuilder(Connection conn, QueryBuilder qb, int pageIndex, int pageSize, out int itemsCount)
		{
			return LoadFromQueryBuilder(conn, qb, pageIndex, pageSize, out itemsCount) ? Items : null;
		}

		/// <summary>
		/// Load page
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="qb"></param>
		/// <param name="pageIndex">starts with 1</param>
		/// <param name="pageSize"></param>
		/// <param name="itemsCount"></param>
		/// <returns></returns>
		public bool LoadFromQueryBuilder(Connection conn, QueryBuilder qb, int pageIndex, int pageSize, out int itemsCount)
		{
			ClearInnerState();
			itemsCount = -1;

			try
			{
				qb.PrepareForSelectQuery(conn, pageIndex, pageSize, out itemsCount, AutoOrderById ? SampleItem.PrimaryKeyFields : null);
				return itemsCount == 0 ? true : LoadFromSql(conn, qb.ToString(), qb.Parameters.ToArray());
			}
			catch (Exception ex)
			{
				OnErrorOccured(conn, new DatabaseException("", ex, "BaseRowList.LoadFromSql(Connection, QueryBuilder, pageIndex, pageSize)", GetType()));
				return false;
			}
		}

		#endregion

		#region Protected virtual functions

		/// <summary>
		/// 
		/// </summary>
		protected virtual QueryBuilder GetQueryBuilder(Connection conn)
		{
			return SampleItem.GetQueryBuilderForSelect(conn);
		}

		/// <summary>
		/// Get query builder for LoadItems function
		/// </summary>
		/// <param name="conn"></param>
		/// <returns></returns>
		protected virtual QueryBuilder GetDefaultQueryBuilder(Connection conn)
		{
			return GetQueryBuilder(conn);
		}

		/// <summary>
		/// By default if error occures, last exception is saved and EventHandler.OnDatabaseException called
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="ex"></param>
		protected virtual void OnErrorOccured(Connection conn, DatabaseException ex)
		{
			lastException = ex;
			conn.OnDatabaseException(ex);
		}

		#endregion

		#region Implementation

		private T samleItem = null;

		/// <summary>
		/// Create item
		/// </summary>
		/// <returns></returns>
		protected abstract T CreateObject();

		/// <summary>
		/// Sample item
		/// </summary>
		public T SampleItem
		{
			get
			{
				if (samleItem == null)
				{
					samleItem = CreateSampleItem();
				}
				return samleItem;
			}
			set
			{
				samleItem = value;
			}
		}

		/// <summary>
		/// Create sample item
		/// </summary>
		/// <returns></returns>
		protected virtual T CreateSampleItem()
		{
			return CreateObject();
		}

		/// <summary>
		/// Clear inner state
		/// </summary>
		protected void ClearInnerState()
		{
			Items.Clear();
			lastException = null;
		}

		#endregion

		#region Sorting

		/// <summary>
		/// Sort by column
		/// </summary>
		/// <param name="column"></param>
		/// <param name="ascendant"></param>
		protected void SortByField(IField column, bool ascendant)
		{
			Items.Sort(new RowComparer(column, ascendant));
		}

		/// <summary>
		/// Sort by columns
		/// </summary>
		/// <param name="columns"></param>
		/// <param name="ascendantList"></param>
		protected void SortByFields(IEnumerable<IField> columns, IList<bool> ascendantList)
		{
			Items.Sort(new RowComparer(columns, ascendantList));
		}

		/// <summary>
		/// Sort by column
		/// </summary>
		/// <param name="fieldName"></param>
		/// <param name="ascendant"></param>
		protected void SortByField(string fieldName, bool ascendant)
		{
			Items.Sort(new RowComparer(new List<string>(fieldName.Split(Connection.FieldNameDelimiter)), ascendant));
		}

		/// <summary>
		/// Sort by columns
		/// </summary>
		/// <param name="fieldList"></param>
		/// <param name="ascendantList"></param>
		protected void SortByFields(IList<string> fieldList, IList<bool> ascendantList)
		{
			IList<IList<string>> fields = new List<IList<string>>();
			foreach (string fld in fieldList)
			{
				fields.Add(fld.Split(Connection.FieldNameDelimiter));
			}
			Items.Sort(new RowComparer(fields, ascendantList));
		}

		/// <summary>
		/// Sort By Property
		/// </summary>
		/// <param name="propertyName"></param>
		/// <param name="ascendant"></param>
		public void SortByProperty(string propertyName, bool ascendant)
		{
			string[] propertyNames = propertyName.Split('.');
			Type type = typeof(T);
			List<PropertyInfo> properties = new List<PropertyInfo>();
			for (int i = 0; i < propertyNames.Length; i++)
			{
				PropertyInfo propInfo = type.GetProperty(propertyNames[i]);
				if (propInfo == null)
					throw new InvalidOperationException("Invalid property name: " + propertyName);
				properties.Add(propInfo);
				type = propInfo.PropertyType;
			}
			Items.Sort(new RowComparer(properties, ascendant));
		}

		/// <summary>
		/// Sort By Properties
		/// </summary>
		/// <param name="propertyNames"></param>
		/// <param name="ascendantList"></param>
		public void SortByProperties(IList<string> propertyNames, IList<bool> ascendantList)
		{
			IList<IList<PropertyInfo>> propertyList = new List<IList<PropertyInfo>>();
			foreach (string propertyName in propertyNames)
			{
				string[] names = propertyName.Split('.');
				Type type = typeof(T);
				List<PropertyInfo> properties = new List<PropertyInfo>();
				for (int i = 0; i < names.Length; i++)
				{
					PropertyInfo propInfo = type.GetProperty(names[i]);
					if (propInfo == null)
						throw new InvalidOperationException("Invalid property name: " + propertyName);
					properties.Add(propInfo);
					type = propInfo.PropertyType;
				}
				propertyList.Add(properties);
			}
			Items.Sort(new RowComparer(propertyList, ascendantList));
		}

		class RowComparer : IComparer<T>
		{
			private readonly IList<IList<string>> fieldList;
			private readonly IList<IList<PropertyInfo>> propertyList;
			private readonly IList<bool> ascendantList;

			#region Constructors

			public RowComparer(IEnumerable<IField> columns, IList<bool> ascendantList)
			{
				this.ascendantList = ascendantList;

				fieldList = new List<IList<string>>();
				foreach (IField column in columns)
				{
					IForeignObject obj = column.Parent.ParentField;
					List<string> fields = new List<string>();
					fields.Add(column.Name);
					while (obj != null)
					{
						fields.Insert(0, obj.Name);
						obj = obj.Parent.ParentField;
					}
					fieldList.Add(fields);
				}
			}

			public RowComparer(IField column, bool ascendant)
				: this(new[] { column }, new[] { ascendant })
			{
			}

			public RowComparer(IList<IList<string>> fieldList, IList<bool> ascendantList)
			{
				this.ascendantList = ascendantList;
				this.fieldList = fieldList;
			}

			public RowComparer(IList<string> field, bool ascendant)
				: this(new[] { field }, new[] { ascendant })
			{
			}

			public RowComparer(IList<IList<PropertyInfo>> propertyList, IList<bool> ascendantList)
			{
				this.ascendantList = ascendantList;
				this.propertyList = propertyList;
			}

			public RowComparer(IList<PropertyInfo> property, bool ascendant)
				: this(new[] { property }, new[] { ascendant })
			{
			}

			#endregion

			#region IComparer<T> Members

			///<summary>
			///Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
			///</summary>
			///
			///<returns>
			///Value Condition Less than zero: x is less than y.Zero: x equals y.Greater than zero: x is greater than y.
			///</returns>
			///
			///<param name="y">The second object to compare.</param>
			///<param name="x">The first object to compare.</param>
			public int Compare(T x, T y)
			{
				if (fieldList != null)
					return CompareObjectsByField(x, y, 0);
				return CompareObjectsByProperty(x, y, 0);
			}

			private int CompareObjectsByField(Row x, Row y, int n)
			{
				int result = CompareObjectsByField(x, y, n, 0);
				if (result != 0 || n + 1 == fieldList.Count)
					return result;
				return CompareObjectsByField(x, y, n + 1);
			}

			private int CompareObjectsByProperty(object x, object y, int n)
			{
				int result = CompareObjectsByProperty(x, y, n, 0);
				if (result != 0 || n + 1 == propertyList.Count)
					return result;
				return CompareObjectsByProperty(x, y, n + 1);
			}

			private int CompareObjectsByField(Row x, Row y, int n, int k)
			{
				string fieldName = fieldList[n][k];
				if (k + 1 == fieldList[n].Count)
				{
					if (ascendantList[n])
						return x.Fields[fieldName].CompareTo(y.Fields[fieldName]);
					return y.Fields[fieldName].CompareTo(x.Fields[fieldName]);
				}
				if (x.Fields.ContainsKey(fieldName) && x.Fields[fieldName] is IForeignObject)
				{
					return CompareObjectsByField(((IForeignObject)x.Fields[fieldName]).GetForeignObject(),
												 ((IForeignObject)y.Fields[fieldName]).GetForeignObject(), n, k + 1);
				}
				throw new InvalidOperationException("invalid field name: " + fieldName);
			}

			private int CompareObjectsByProperty(object x, object y, int n, int k)
			{
				PropertyInfo propInfo = propertyList[n][k];

				object v1 = propInfo.GetValue(x, new object[] { });
				object v2 = propInfo.GetValue(y, new object[] { });

				if (k + 1 == propertyList[n].Count)
				{
					if (!(v1 is IComparable) || !(v2 is IComparable))
						throw new InvalidOperationException("not comparable");
					if (ascendantList[n])
						return ((IComparable)v1).CompareTo(v2);
					return ((IComparable)v2).CompareTo(v1);
				}
				return CompareObjectsByProperty(v1, v2, n, k + 1);
			}

			#endregion
		}

		#endregion

		#region Public Load Functions

		/// <summary>
		/// Load items
		/// </summary>
		/// <param name="conn"></param>
		/// <returns></returns>
		public List<T> LoadItems(Connection conn)
		{
			return Load(conn) ? Items : null;
		}

		/// <summary>
		/// Load items (with paging)
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="pageIndex"></param>
		/// <param name="pageSize"></param>
		/// <param name="itemsCount"></param>
		/// <returns></returns>
		public List<T> LoadItems(Connection conn, int pageIndex, int pageSize, out int itemsCount)
		{
			return Load(conn, pageIndex, pageSize, out itemsCount) ? Items : null;
		}

		/// <summary>
		/// Load items
		/// </summary>
		/// <param name="conn"></param>
		/// <returns></returns>
		public virtual bool Load(Connection conn)
		{
			return LoadFromQueryBuilder(conn, GetDefaultQueryBuilder(conn));
		}

		/// <summary>
		/// Load items (with paging)
		/// </summary>
		/// <param name="conn"></param>
		/// <param name="pageIndex"></param>
		/// <param name="pageSize"></param>
		/// <param name="itemsCount"></param>
		/// <returns></returns>
		public virtual bool Load(Connection conn, int pageIndex, int pageSize, out int itemsCount)
		{
			return LoadFromQueryBuilder(conn, GetDefaultQueryBuilder(conn), pageIndex, pageSize, out itemsCount);
		}

		#endregion
	}

}

